/*

Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016.  All rights reserved.

Contact:
     SYSTAP, LLC DBA Blazegraph
     2501 Calvert ST NW #106
     Washington, DC 20008
     licenses@blazegraph.com

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; version 2 of the License.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

*/
/*
 * Created on Sep 19, 2008
 */

package com.bigdata.io;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.UUID;

import org.apache.log4j.Logger;

import com.bigdata.journal.AbstractJournal;
import com.bigdata.journal.FileMetadata;
import com.bigdata.journal.Options;
import com.bigdata.resources.StoreManager;

/**
 * Utility methods for managing exlusive {@link FileLock}s and advisory locks
 * depending on what is supported by the platform, file access mode, and volume
 * on which the file resides.
 * 
 * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a>
 * @version $Id$
 * 
 * @deprecated This can lead to trouble. Use a {@link FileLock} if supported and
 *             otherwise proceed without a {@link FileLock}.
 */
public class FileLockUtility {

    protected static final Logger log = Logger.getLogger(FileLockUtility.class);
    protected static final boolean INFO = log.isInfoEnabled();
    protected static final boolean DEBUG = log.isDebugEnabled();

    /**
     * Create/open the file and obtain an exclusive lock.
     * <p>
     * A {@link FileLock} will be used when supported and requested. An advisory
     * lock will be used if <i>useFileLock == false</code>, if the <i>fileMode</i>
     * is read-only. If tryLock() returns <code>null</code> then the lock
     * exists and this request will fail. However, if
     * {@link FileChannel#tryLock()} throws an {@link IOException} then the
     * underlying platform does not support {@link FileLock} for the named file
     * (memory mapped files, read-only files, and NFS mounted files can all have
     * this problem) and we will attempt to acquire an advisory lock instead.
     * 
     * <strong>Advisory locks are NOT visible to other applications</strong>
     * 
     * <p>
     * Do NOT request a {@link FileLock} if you are going to use a memory-mapped
     * buffer. The JDK cautions that these things do not play well together on
     * some platforms.
     * 
     * @param file
     *            The file.
     * @param fileMode
     *            The file mode for
     *            {@link RandomAccessFile#RandomAccessFile(File, String)}
     * @param useFileLock
     *            <code>true</code> if {@link FileChannel#tryLock()} should be
     *            attempted. when <code>false</code> only an advisory lock
     *            will be sought.
     * 
     * @return The {@link RandomAccessFile}
     * 
     * @throws IOException
     *             If the file could not be opened or someone already holds a
     *             lock for that file.
     * 
     * @see FileMetadata#acquireAdvisoryLock(File)
     * 
     * @see Options#FILE_LOCK_ENABLED
     * 
     * @todo we really don't need locks for temporary files.
     * 
     * @todo handle lock files during {@link StoreManager} startup.
     * 
     * @todo if you use a {@link FileLock} to open a file then you are
     *       protected.
     * 
     * @todo this ignores locks for read only file modes. in fact we should
     *       create the file with a read-only marker and allow more than one
     *       process to obtain that lock.
     */
    public static RandomAccessFile openFile(File file, String fileMode,
            boolean useFileLock) throws IOException {
        
        final boolean readOnly = "r".equals(fileMode);
    
        final RandomAccessFile raf = new RandomAccessFile(file, fileMode);
        
        if(readOnly) return raf;
    
        // Note: a System property.
        final boolean fileLockEnabled = Boolean.parseBoolean(System
                .getProperty(Options.FILE_LOCK_ENABLED,
                        Options.DEFAULT_FILE_LOCK_ENABLED));
    
        if (useFileLock && fileLockEnabled) {//bufferMode != BufferMode.Mapped) {
    
            if (INFO)
                log.info("Seeking exclusive lock: " + file.getAbsolutePath());
            
            if (new File(file + ".lock").exists()) {

                // reject if there is already an advisory lock for this file.
            	
            	raf.close(); // but close file first!
                
                throw new IOException("Advisory lock exists: "
                        + file.getAbsolutePath());
                
            }
            
            try {
                
                // seek a native platform exclusive file lock.
                if (raf.getChannel().tryLock() != null) {
    
                    // got it.
                    return raf;
                    
                } else {
                    
                    /*
                     * A null return indicates that someone else holds the lock.
                     */
                    try {
                        
                        raf.close();
                        
                    } catch (Throwable t) {
                        
                        // log and ignore.
                        log.error(t, t);
                        
                    }
    
                    /*
                     * We were not able to get a lock on the file.
                     */
                    throw new RuntimeException("Already locked: "
                            + file.getAbsoluteFile());
    
                }
                
            } catch (IOException ex) {
                
                /*
                 * The platform does not support FileLock (memory mapped files,
                 * read-only files, NFS mounted files all have this problem).
                 */
                log.warn("FileLock not supported: file="
                        + file.getAbsolutePath() + " : " + ex);
    
                return _acquireAdvisoryLock(raf,file);
                
            }
    
        } else {
            
            /*
             * FileLock not requested or explicitly disabled for the JVM.
             */
    
            return _acquireAdvisoryLock(raf, file);
            
        }
    
    }

    private static RandomAccessFile _acquireAdvisoryLock(RandomAccessFile raf,
            File file) throws IOException {

        try {
            
            // seek an advisory lock.
            if (acquireAdvisoryLock(file)) {

                // obtained advisory lock.
                return raf;
            }
            
            // someone else holds the advisory lock.
            try {
                raf.close();
            } catch (IOException t) {
                // log and ignore.
                log.error(t, t);
            }

            throw new IOException("Advisory lock exists: " + file.getAbsolutePath());
            
        } catch (IOException ex2) {

            log.error("Error while seeking advisory lock: file=" + file.getAbsolutePath(), ex2);
            
            try {
                raf.close();
            } catch (IOException t) {
                // log and ignore.
                log.error(t, t);
            }
            throw ex2;
            
        }

    }
    
    /**
     * Close the file and automatically releases the {@link FileLock} (if any)
     * and removes the advisory lock for that file (if any).
     * <p>
     * Note: This method should be used in combination with
     * {@link FileLockUtility#openFile(File, String, boolean)} in order to ensure that the
     * optional advisory lock file is deleted when the file is closed. The
     * purpose of the advisory lock file is to provide advisory locking file
     * modes (read-only), platforms, or file systems (NFS) that do not support
     * {@link FileLock}.
     * 
     * @param file
     *            The file.
     * @param raf
     *            The {@link RandomAccessFile}.
     * 
     * @throws IOException
     */
    public static void closeFile(File file, RandomAccessFile raf)
            throws IOException {

        if (file == null)
            throw new IllegalArgumentException();
        
        if (raf == null)
            throw new IllegalArgumentException();
        
        try {

            if (raf.getChannel().isOpen()) {

                /*
                 * close the file iff open.
                 * 
                 * Note: a thread that is interrupted during an IO can cause the
                 * file to be closed asynchronously. This is handled by the
                 * disk-based store modes.
                 */
                raf.close();

            }

        } finally {

            /*
             * Remove the advisory lock (if present) regardles of whether the
             * file is currently open (see note above).
             */
            removeAdvisoryLock(file);

        }

    }

    /**
     * Creates an advisory lock file having the same basename as the given file
     * with a <code>.lock</code> extension.
     * <p>
     * Note: This uses {@link File#createNewFile()} which is NOT advised for
     * this purpose. However, {@link FileLock} does not work in some contexts so
     * this is used as a fallback mechanism. We write a {@link UUID} into the
     * advisory lock since Java does not have platform independent PIDs. That
     * {@link UUID} allows us to tell whether the advisory lock file was created
     * by this process or by another process.
     * <p>
     * Note: If a {@link Thread} is interrupted during an NIO operation then the
     * {@link FileChannel} will be closed asynchronously. While this correctly
     * releases a {@link FileLock} it does NOT cause our advisory lock file to
     * be deleted. During a normal shutdown of an {@link AbstractJournal}, the
     * advisory lock file is deleted by
     * {@link #closeFile(File, RandomAccessFile)}. However, following an
     * abnormal shutdown the advisory lock file MAY still exist and (assuming
     * that {@link FileLock} is not working since we created an advisory lock in
     * the first place) it MUST be removed by hand before the
     * {@link AbstractJournal} can be reopened.
     * 
     * @param file
     *            The given file.
     * 
     * @return <code>true</code> if the advisory lock was created or exists
     *         and was created by this process. <code>false</code> if the
     *         advisory lock already exists and was created by another process.
     * 
     * @throws IOException
     *             If there is a problem.
     * 
     * @see #pid
     */
    synchronized public static boolean acquireAdvisoryLock(File file)
            throws IOException {

        if (INFO)
            log.info("Seeking advisory lock: " + file.getAbsolutePath());
        
        final File lockFile = new File(file + ".lock");

        if(lockFile.exists()) {
            
            // check the signature in the lock file.
            return isOurLockFile(lockFile);
            
        }
        
        if(!lockFile.createNewFile()) {
            
            // someone else got there first.
            return false;
            
        }
        
        {
            
            final BufferedWriter w = new BufferedWriter(
                    new FileWriter(lockFile));

            try {

                w.write(pid);

                w.write('\n');

                w.flush();
                
            } finally {

                w.close();

            }
            
        }
        
        if (INFO)
            log.info("Created advisory lock: " + file.getAbsolutePath());
        
        return true;

    }

    static public boolean isOurLockFile(File lockFile) throws IOException {

        final BufferedReader r = new BufferedReader(new FileReader(lockFile));

        try {

            final String str = r.readLine();

            if (pid.equals(str))
                return true;

            return false;

        } finally {

            r.close();
            
        }
        
    }
    
    /**
     * Removes the advisory lock for the file if it exists.
     * 
     * @param file
     *            The file whose <code>.lock</code> file will be removed.
     * 
     * @throws IOException
     *             if the lock file exists but does not belong to this process
     *             or can not be removed.
     * 
     * @see #acquireAdvisoryLock(File)
     */
    synchronized public static void removeAdvisoryLock(File file)
            throws IOException {

        final File lockFile = new File(file + ".lock");

        // no advisory lock file.
        if (!lockFile.exists()) return;
        
        if (!isOurLockFile(lockFile)) {
            
            throw new IOException("Not our lock file: " + lockFile.getAbsolutePath());
            
        }

        if (!lockFile.delete()) {

            throw new IOException("Could not delete lock file: " + lockFile.getAbsolutePath());

        }


    }

    /**
     * Since Java does not have platform independent PIDs we use a static
     * {@link UUID} to identify this process. This {@link UUID} gets written
     * into all advisory lock files that the process creates. Another process
     * should check the {@link UUID} in the advisory lock file and refuse to
     * open the file if the {@link UUID} is not its own {@link UUID}.
     */
    static String pid = UUID.randomUUID().toString();
    
}
